/****************************************************************************
* Copyright (c) 2004-2006 Jeremy Dowdall
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*    Jeremy Dowdall <jeremyd@aspencloud.com> - initial API and implementation
*****************************************************************************/

package org.aspencloud.widgets.cdatepicker;

import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.aspencloud.widgets.ACW;
import org.aspencloud.widgets.CButton;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TypedListener;


/**
 * The CDatepicker is a component which allows a user to set the attributes of a 
 * java.util.Date class in a graphical manner.  The java.util.Date class contains
 * both date and time information and thus the CDatepicker has two components: a 
 * Calendar and a Clock.  The Calendar is a monthly calendar in which each day is 
 * a button that can be selected.  The Clock is a GC drawn analog clock whose hands 
 * can be dragged to the appropriate locations to select the desired time.
 * The two can be visible one at a time, or together depending upon the style setting.
 * <dt><b>Styles:</b></dt>
 * <dd>BORDER, DATE, DATE_TIME, FOOTER, TIME</dd>
 * <dt><b>Events:</b></dt>
 * <dd>Selection</dd>
 * @see #addSelectionListener(SelectionListener)
 */
public class CDatepicker extends Composite {
	
	private static final int NOTIFY_NONE 	= 0;
	private static final int NOTIFY_REGULAR 	= 1;
	private static final int NOTIFY_DEFAULT 	= 2;

	private static final String DATE = "date";
	private static final String DAY 	= "day";
	private static final int DAYS_IN_WEEK = 7;
	private static final int NUM_ROWS = 6;

	private static Calendar tmpcal;

	/**
	 * The constructor is passed constants from the ACW class which must be converted
	 * to their corresponding SWT class constants for use in the super's constructor.
	 * No, this is not ideal, but does eliminate collisions during development.  Expect
	 * changes to this in the future.
	 * @param style constants from ACW.java
	 * @return constants from SWT.java
	 */
	private static int swtStyle(int style) {
		if((ACW.BORDER & style) != 0) return SWT.BORDER;
		return SWT.NONE;
	}

	private Composite calendarComposite;
	private Composite header;
	private CButton monthLabel;
	private CButton monthPrev;
	private CButton calendarNow;
	private CButton monthNext;
	private Composite yearComposite;
	private Text yearText;
	private CButton yearLabel;
	private CButton yearEditAccept;
	private CButton yearEditCancel;
	private Composite body;
	private Label dayLabels[];
	private CButton dayButtons[];
	private CButton footerButton;
	private MenuItem bodyItem;
	private MenuItem[] monthItems = new MenuItem[12];

	private Composite clockComposite;
	private CButton clockNow;
	private CButton clockAmPm;
	private CButton clockAccept;
	private CButton clockCancel;
	private int clockRadius;
	private Point clockCenter = new Point(0,0);

	private Locale locale;
	private Date cancelDate;
	private int selDayButton;

	private int hourOfDay = 0;
	private int minute = 0;
	private int second = 0;
	private boolean setH = false;
	private boolean setM = false;
	private boolean setS = false;
	private boolean overHour = false;
	private boolean overMin = false;
	private boolean overSec = false;
	private boolean secHandDefault = true;
	private boolean secHand = true;
	private int[] timeSnap = { 1, 1 };
	private long timeInc = 300000; // 5 minutes

	private int format;
	private boolean footerVisible = false;
	private boolean gridVisible = false;
	private GridLayout baseLayout;
	
	private List yearEditControls;
	private List controls;
	private Listener filter;
	private boolean hasFocus = false;
	private boolean editYear = false;

	private boolean comboMode = false;

	/**
	 * Constructs a new instance of this class given its parent and a style value 
	 * describing its behavior and appearance.
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 */
	public CDatepicker(Composite parent, int style) {
		this(parent, style, null, null);
	}
	/**
	 * Constructs a new instance of this class given its parent, a style value 
	 * describing its behavior and appearance, and a date to which the initial selection
	 * will be set.
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 * @param date a Date object representing the initial selection
	 */
	public CDatepicker(Composite parent, int style, Date date) {
		this(parent, style, date, null);
	}
	/**
	 * Constructs a new instance of this class given its parent, a style value 
	 * describing its behavior and appearance, a date to which the initial selection
	 * will be set, and the locale to use.
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 * @param date a Date object representing the initial selection
	 * @param locale the locale which this CDatepicker is to use
	 */
	public CDatepicker(Composite parent, int style, Date date, Locale locale) {
		super(parent, swtStyle(style));
		this.format = style;
		this.footerVisible = (style & ACW.FOOTER) != 0;
		this.secHand = this.secHandDefault = (style & ACW.SECONDS_OFF) == 0;
		this.locale = (locale == null) ? Locale.getDefault() : locale;
		tmpcal = Calendar.getInstance(this.locale);

		setBackground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW));
		
		// setup self composite
		baseLayout = new GridLayout(2, true);
		baseLayout.marginHeight = 0;
		baseLayout.marginWidth = 0;
		baseLayout.horizontalSpacing = 1;
		baseLayout.verticalSpacing = 0;
		setLayout(baseLayout);

		createCalendar();
		createClock();
		setFormat(style);
		setDays(tmpcal.getTime(), true);
		setSelection((date == null) ? new Date() : date, true);
		updateLocale();
		
		filter = new Listener() {
			public void handleEvent(Event event) {
				handleFocus(SWT.FocusOut);
			}
		};

		addPaintListener(new PaintListener() {
			public void paintControl(PaintEvent e) {
				e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
				Rectangle rect = calendarComposite.getBounds();
				e.gc.drawLine(rect.x+rect.width, 0, rect.x+rect.width, rect.y+rect.height);
			}
		});
		
		addListener(SWT.Dispose, new Listener() {
			public void handleEvent(Event event) {
				getDisplay().removeFilter(SWT.FocusIn, filter);
			}
		});
		
		controls = ACW.getControls(this);
		for(Iterator i = controls.iterator(); i.hasNext(); ) {
			((Control) i.next()).addFocusListener(new FocusListener() {
				public void focusGained(FocusEvent e) {
					handleFocus(SWT.FocusIn);
				}
				public void focusLost(FocusEvent e) {
					handleFocus(SWT.FocusOut);
				}
			});
		}
		
		yearEditControls = new ArrayList(ACW.getControls(yearComposite));
		yearEditControls.addAll(ACW.getControls(yearLabel));
		yearEditControls.addAll(ACW.getControls(yearEditAccept));
		yearEditControls.addAll(ACW.getControls(yearEditCancel));
	}
	/**
	 * Constructs a new instance of this class given its parent, a style value 
	 * describing its behavior and appearance, and the locale to use.
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 * @param locale the locale which this CDatepicker is to use
	 */
	public CDatepicker(Composite parent, int style, Locale locale) {
		this(parent, style, null, locale);
	}

	/**
	 * Adds the listener to the collection of listeners who will be notified when the 
	 * receiver's selection changes, by sending it one of the messages defined in the 
	 * <code>SelectionListener</code> interface.
	 * <p>
	 * <b><code>widgetSelected</code></b> is called when a "non-committal" selection of a date or time
	 * is made.<br>
	 * For the Calendar, this is whenever the visible days are changed and the selection
	 * is thus modified only so that it remains visible (anything except a direct selection of a 
	 * <code>dayButton</code> or a Carriage Return on a <code>dayButton</code>).<br>
	 * For the Clock, this is any selection or than the "Accept" or "Cancel" buttons, which are
	 * only visible when in "Combo" mode ({@link #setComboMode()}).
	 * <b><code>widgetDefaultSelected</code></b> is called when a "committal" selection of a date or time
	 * is made.<br>
	 * For the Calendar this is when a <code>dayButton</code> is selected.<br>
	 * For the Clock this is when a "Accept" or "Cancel" button is selected.
	 * </p>
	 * @param listener the listener which should be notified
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
	 * </ul>
	 * @exception SWTException <ul>
	 *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
	 *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
	 * </ul>
	 * @see SelectionListener
	 * @see #removeSelectionListener
	 * @see SelectionEvent
	 */
	public void addSelectionListener(SelectionListener listener) {
		checkWidget ();
		if(listener != null) {
			TypedListener typedListener = new TypedListener (listener);
			addListener (SWT.Selection, typedListener);
			addListener (SWT.DefaultSelection, typedListener);
		}
	}
	
	/**
	 * Modifies the given Calendar field by the given amount for every dayButton.<br/>
	 * calendar.add(CalendarField, amount)
	 * @param field Calendar Field
	 * @param amount adjustment to be added
	 */
	private void adjustDays(int field, int amount) {
		for(int day = 0; day < dayButtons.length; day++) {
			tmpcal.setTime((Date) dayButtons[day].getData(DATE)); 
			tmpcal.add(field, amount);
			dayButtons[day].setData(DATE, tmpcal.getTime());
		}
	}

	/**
	 * if <code>cancelDate</code> is not null, cancels the edit by setting the selection
	 * to <code>cancelDate</code>.  also fires a default selection event notification and
	 * sets <code>cancelDate</code> to null.
	 */
	private void cancel() {
		if(cancelDate != null) {
			setSelection(cancelDate);
			cancelDate = null;
			clockCancel.setEnabled(false);
		}
		fireSelectionChanged(NOTIFY_DEFAULT);
	}
	
	/**
	 * create the Calendar part of this CDatepicker
	 */
	private void createCalendar() {
		calendarComposite = new Composite(this, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 0;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		calendarComposite.setLayout(layout);
		calendarComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		
		createCalendarHeader();
		
		Label separator = new Label(calendarComposite, SWT.HORIZONTAL | SWT.SEPARATOR);
		separator.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		
		createCalendarBody();
		createCalendarFooter();
	}

	/**
	 * create the Calendar's body, which contains the dayLabels and dayButtons
	 */
	private void createCalendarBody() {
		body = new Composite(calendarComposite, SWT.NONE);
		body.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
		GridLayout layout = new GridLayout(7, true);
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		layout.marginBottom = 1;
		layout.horizontalSpacing = 1;
		layout.verticalSpacing = 1;
		body.setLayout(layout);
		body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		body.addListener(SWT.MouseWheel, new Listener() {
			public void handleEvent(Event event) {
				if(ACW.win32) {
					Point pt = getDisplay().getCursorLocation();
					if(monthLabel.getClientArea().contains(monthLabel.toControl(pt))) {
						tmpcal.setTime(getSelection());
						tmpcal.add(Calendar.MONTH, (event.count > 0) ? 1 : -1);
						setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
					} else if(yearLabel.getClientArea().contains(yearLabel.toControl(pt))) {
						tmpcal.setTime(getSelection());
						tmpcal.add(Calendar.YEAR, (event.count > 0) ? 1 : -1);
						setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
					} else if(clockComposite.getClientArea().contains(clockComposite.toControl(pt))) {
						clockComposite.notifyListeners(SWT.MouseWheel, event);
					} else {
						scrollCalendar((event.count > 0) ? SWT.ARROW_UP : SWT.ARROW_DOWN);
					}
				} else {
					scrollCalendar((event.count > 0) ? SWT.ARROW_UP : SWT.ARROW_DOWN);
				}
			}
		});
		body.addPaintListener(new PaintListener() {
			public void paintControl(PaintEvent e) {
				e.gc.setBackground(dayLabels[0].getBackground());
				e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
				Rectangle rect = body.getClientArea();
				if(!gridVisible) {
					rect.height = dayLabels[0].getSize().y;
				}
				e.gc.fillRectangle(rect);
				int y = dayLabels[0].getSize().y;
				e.gc.drawLine(rect.x, y, rect.x+rect.width, y);
				if(footerVisible) {
					y = rect.y + body.getClientArea().height - 1;
					e.gc.drawLine(rect.x, y, rect.x+rect.width, y);
				}
			}
		});

		Menu bodyMenu = new Menu(body);
		bodyItem = new MenuItem(bodyMenu, SWT.NONE);
		bodyItem.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				tmpcal.setTime(new Date());
				setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
			}
		});
		body.setMenu(bodyMenu);

		dayLabels = new Label[DAYS_IN_WEEK];
		for(int day = 0; day < dayLabels.length; day++) {
			dayLabels[day] = new Label(body, SWT.CENTER);
			dayLabels[day].setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
			dayLabels[day].addMouseListener(new MouseAdapter() {
				public void mouseDown(MouseEvent e) {
					if(editYear) setEditYearMode(false, true);
				}
			});
		}
		
		dayButtons = new CButton[DAYS_IN_WEEK * NUM_ROWS];
		for(int day = 0; day < dayButtons.length; day++) {
			// TODO: clean up dayButton listeners
			CButton dayButton = new CButton(body, SWT.TOGGLE);
			dayButtons[day] = dayButton;
			dayButton.setSquare(true);
			dayButton.setMargins(4, 4);
			dayButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
			dayButton.setData(DAY, new Integer(day));
//			dayButton.addPaintListener(new PaintListener() {
//				public void paintControl(PaintEvent e) {
//					Point s = ((Control) e.widget).getSize();
//					e.gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
//					e.gc.drawRectangle(0, 0, s.x-1, s.y-1);
//				}
//			});
			dayButton.addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent e) {
					Button button = (Button) e.widget;
					setSelection(((Integer) button.getParent().getData(DAY)).intValue(), NOTIFY_DEFAULT);
				}
			});
			dayButton.getButton().addKeyListener(new KeyAdapter() {
				public void keyPressed(KeyEvent e) {
					// block the arrow keys because they are handled by the traverse listener
					if((e.keyCode != SWT.ARROW_DOWN) && (e.keyCode != SWT.ARROW_UP)) {
						scrollCalendar(e.keyCode);
					}
				}
			});
			dayButton.getButton().addMouseListener(new MouseAdapter() {
				public void mouseDown(MouseEvent e) {
					if(e.button == 3) {
						body.getMenu().setVisible(true);
					}
				}
			});
			dayButton.getButton().addTraverseListener(new TraverseListener() {
				public void keyTraversed(TraverseEvent e) {
					if((e.keyCode == SWT.KEYPAD_CR) ||
							(e.keyCode == '\n') ||
							(e.keyCode == '\r')) {
						fireSelectionChanged(NOTIFY_DEFAULT);
					}
					traverseSelection(e.keyCode);
				}
			});
		}
	}

	/**
	 * create the footer (footerButton) for the Calendar part of this CDatepicker<br/>
	 * there is currently no footer for the Clock part - should there be?  or
	 * should this footer span both parts?
	 */
	private void createCalendarFooter() {
		footerButton = new CButton(calendarComposite, SWT.CENTER);
		GridData data = new GridData(SWT.FILL, SWT.CENTER, false, false);
		if(!footerVisible) data.exclude = true;
		footerButton.setLayoutData(data);
		footerButton.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				setSelection(new Date(), true, NOTIFY_REGULAR);
			}
		});
	}

	/**
	 * create the header for the Calendar part of this CDatepicker<br/>
	 * there is no equivalent for the Clock part
	 */
	private void createCalendarHeader() {
		header = new Composite(calendarComposite, SWT.NONE);
		GridLayout layout = new GridLayout(5, false);
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 0;
		layout.marginHeight = 2;
		layout.marginWidth = 2;
		header.setLayout(layout);
		header.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));

		monthPrev = new CButton(header, SWT.ARROW | SWT.LEFT, getDisplay().getSystemColor(SWT.COLOR_GRAY));
		monthPrev.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		monthPrev.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				tmpcal.setTime(getSelection());
				tmpcal.add(Calendar.MONTH, -1);
				setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
			}
		});

		calendarNow = new CButton(header, SWT.NONE);
		calendarNow.setMargins(4, 0);
		calendarNow.setPolygon(new int[] { 7,7 }, getDisplay().getSystemColor(SWT.COLOR_GRAY));
		calendarNow.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		calendarNow.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				setSelection(new Date(), true, NOTIFY_REGULAR);
			}
		});
		
		monthNext = new CButton(header, SWT.ARROW | SWT.RIGHT, getDisplay().getSystemColor(SWT.COLOR_GRAY));
		monthNext.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		monthNext.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				tmpcal.setTime(getSelection());
				tmpcal.add(Calendar.MONTH, 1);
				setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
			}
		});

		monthLabel = new CButton(header, SWT.NONE);
		monthLabel.setAlignment(SWT.RIGHT, SWT.CENTER);
		GridData data = new GridData(SWT.FILL, SWT.FILL, true, false);
		data.horizontalIndent = 2;
		monthLabel.setLayoutData(data);
		if(ACW.gtk) {
			monthLabel.getButton().addListener(SWT.MouseWheel, new Listener() {
				public void handleEvent(Event event) {
					if(SWT.MouseWheel == event.type) {
						tmpcal.setTime(getSelection());
						tmpcal.add(Calendar.MONTH, (event.count > 0) ? 1 : -1);
						setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
					}
				}
			});
		}
		monthLabel.getButton().addMouseListener(new MouseAdapter() {
			public void mouseDown(MouseEvent e) {
				monthLabel.getMenu().setVisible(true);
			}
		});

		Menu monthMenu = new Menu(monthLabel);
		for(int i = 0; i < 12; i++) {
			monthItems[i] = new MenuItem(monthMenu, SWT.NONE);
			tmpcal.set(Calendar.MONTH, i);
			monthItems[i].setData("Month", new Integer(tmpcal.get(Calendar.MONTH)));
			monthItems[i].addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent e) {
					MenuItem item = (MenuItem) e.widget;
					tmpcal.setTime(getSelection());
					tmpcal.set(Calendar.MONTH, ((Integer) item.getData("Month")).intValue());
					setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
				}
			});
		}
		monthLabel.setMenu(monthMenu);

		yearComposite = new Composite(header, SWT.NONE);
		layout = new GridLayout();
		layout.marginWidth = 2;
		layout.marginHeight = 2;
		yearComposite.setLayout(layout);
		data = new GridData();
		data.exclude = true;
		yearComposite.setLayoutData(data);
		yearComposite.setVisible(false);
		yearComposite.addPaintListener(new PaintListener() {
			public void paintControl(PaintEvent e) {
				e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_GRAY));
				Rectangle r = yearComposite.getClientArea();
				e.gc.drawRectangle(r.x, r.y, r.width-1, r.height-1);
			}
		});
		
		yearText = new Text(yearComposite, SWT.CENTER | SWT.SINGLE);
		yearText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		yearText.addListener(SWT.MouseWheel, new Listener() {
			public void handleEvent(Event event) {
				if(SWT.MouseWheel == event.type) {
					setEditYearMode(false, true);
					tmpcal.setTime(getSelection());
					tmpcal.add(Calendar.YEAR, (event.count > 0) ? 1 : -1);
					setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
				}
			}
		});
		yearText.addTraverseListener(new TraverseListener() {
			public void keyTraversed(TraverseEvent e) {
				if(e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
					setEditYearMode(false, false);
				} else if(e.keyCode == SWT.ESC) {
					setEditYearMode(false, true);
					e.doit = false;
				}
			}
		});
		header.addMouseListener(new MouseAdapter() {
			public void mouseDown(MouseEvent e) {
				if(editYear) setEditYearMode(false, true);
			}
		});
		yearText.addVerifyListener(new VerifyListener() {
			public void verifyText(VerifyEvent e) {
				if(e.text == null || e.text.length() == 0) {
					e.doit = !Character.isLetter(e.character);
				} else {
					try {
						Integer.parseInt(e.text);
					} catch (NumberFormatException nfe) {
						e.doit = false;
					}
				}
			}	
		});
		
		yearLabel = new CButton(header, SWT.NONE);
		yearLabel.setLayoutData(new GridData(SWT.FILL, SWT.BOTTOM, false, false));
		if(ACW.gtk) {
			yearLabel.getButton().addListener(SWT.MouseWheel, new Listener() {
				public void handleEvent(Event event) {
					if(SWT.MouseWheel == event.type) {
						tmpcal.setTime(getSelection());
						tmpcal.add(Calendar.YEAR, (event.count > 0) ? 1 : -1);
						setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
					}
				}
			});
		}
		yearLabel.getButton().addMouseListener(new MouseAdapter() {
			public void mouseDown(MouseEvent e) {
				setEditYearMode(true, false);
			}
		});

		yearEditAccept = new CButton(header, SWT.OK);
		data = new GridData(SWT.FILL, SWT.FILL, false, false);
		data.horizontalIndent = 2;
		data.exclude = true;
		yearEditAccept.setLayoutData(data);
		yearEditAccept.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				setEditYearMode(false, false);
			}
		});

		yearEditCancel = new CButton(header, SWT.CANCEL);
		data = new GridData(SWT.FILL, SWT.FILL, false, false);
		data.exclude = true;
		yearEditCancel.setLayoutData(data);
		yearEditCancel.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				setEditYearMode(false, true);
			}
		});
	}

	/**
	 * create the Clock part of this CDatepicker
	 */
	private void createClock() {
		clockComposite = new Composite(this, ACW.gtk ? SWT.NONE : SWT.DOUBLE_BUFFERED);
		clockComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		clockComposite.setLayout(new Layout() {
			protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
				return calendarComposite.computeSize(wHint, hHint, flushCache);
			}
			protected void layout(Composite composite, boolean flushCache) {
				Rectangle r = clockComposite.getClientArea();
				clockRadius = (r.width - 10) / 2;
				clockCenter.x = r.width / 2;
				clockCenter.y = r.height / 2;
				
				clockNow.setBounds(clockCenter.x-11, clockCenter.y-11, 22, 22);

				Point size = clockNow.getSize();
				clockAmPm.setBounds(
						clockCenter.x-(size.x/2),
						clockCenter.y+(clockRadius/3)-(size.y/2),
						size.x,
						size.y
						);
				
				if(comboMode) {
					size = clockCancel.computeSize(-1, -1);
					clockCancel.setBounds(r.x+r.width-size.x, r.y, size.x, size.y);
					clockAccept.setBounds(r.x+r.width-(2*size.x+1), r.y, size.x, size.y);
				}
			}
		});

		clockNow = new CButton(clockComposite, SWT.NONE);
		clockNow.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
		clockNow.setFillColor(getDisplay().getSystemColor(SWT.COLOR_GRAY));
		clockNow.setMargins(4, 4);
		clockNow.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
		clockNow.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				setSelection(new Date(), false, NOTIFY_REGULAR);
			}
		});

		clockAmPm = new CButton(clockComposite, SWT.NONE);
		clockAmPm.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
		clockAmPm.setMargins(4, 4);
		Listener tapl = new Listener() {
			public void handleEvent(Event event) {
				tmpcal.setTime(getSelection());
				tmpcal.set(Calendar.AM_PM, (tmpcal.get(Calendar.AM_PM) == 0) ? 1 : 0);
				setSelection(tmpcal.getTime(), false, NOTIFY_REGULAR);
			}
		};
		clockAmPm.addListener(SWT.Selection, tapl);
		clockAmPm.addListener(SWT.MouseWheel, tapl);
		
		clockAccept = new CButton(clockComposite, SWT.OK);
		clockAccept.setVisible(false);
		clockAccept.setMargins(5, ACW.gtk ? 10 : 8);
		clockAccept.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
		clockAccept.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				fireSelectionChanged(NOTIFY_DEFAULT);
			}
		});

		clockCancel = new CButton(clockComposite, SWT.CANCEL);
		clockCancel.setVisible(false);
		clockCancel.setMargins(5, ACW.gtk ? 10 : 8);
		clockCancel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
		clockCancel.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				cancel();
			}
		});
		
		final Listener listener = new Listener() {
			public void handleEvent(Event event) {
				switch(event.type) {
				case SWT.MouseDown:
					if(overHour) {
						setH = true;
					} else if(overMin) {
						setM = true;
					} else if(overSec) {
						setS = true;
					}
					if(setH || setM || setS) {
						clockComposite.setCursor(event.display.getSystemCursor(SWT.CURSOR_SIZEALL));
					}
					break;
				case SWT.MouseMove:
					tmpcal.setTime(getSelection());
					int dx = event.x - clockCenter.x;
					int dy = event.y - clockCenter.y;
					double val;
					if(dx == 0) {
						if(dy > 0) val = 30;
						else val = 0;
					} else if(dy == 0) {
						if(dx > 0) val = 15;
						else val = 45;
					} else {
						val = (30*Math.atan((double)dy/(double)dx) / Math.PI) + 15;
						if(dx < 0) 
							val += 30;
					}
					if(setH) {
						int v = (int) ((val / 5) - (double) tmpcal.get(Calendar.MINUTE) / 60 + .5);
						if(v > 11) v = 11;
						tmpcal.set(Calendar.HOUR, v);
						setSelection(tmpcal.getTime(), false, NOTIFY_REGULAR);
					} else if(setM) {
						int v = (int) (val + 0.5);
						if(v > 59) v = 59;
						tmpcal.set(Calendar.MINUTE, v);
						setSelection(tmpcal.getTime(), false, NOTIFY_REGULAR);
					} else if(setS) {
						int v = (int) (val + 0.5);
						if(v > 59) v = 59;
						tmpcal.set(Calendar.SECOND, v);
						setSelection(tmpcal.getTime(), false, NOTIFY_REGULAR);
					} else {
						boolean rd = false;
						if(overHour || overMin || overSec) rd = true;
						overHour = false;
						overMin = false;
						overSec = false;
						if(Math.sqrt(dx*dx + dy*dy) < clockRadius) {
							double h = (tmpcal.get(Calendar.HOUR) + 
									(double) tmpcal.get(Calendar.MINUTE) / 60) * 5;
							int m = tmpcal.get(Calendar.MINUTE);
							int s = tmpcal.get(Calendar.SECOND);
							if(val-1 < h && h <= val+1) {
								overHour = true;
								rd = true;
							} else if(val-1 < m && m <= val+1) {
								overMin = true;
								rd = true;
							} else if(secHand && val-1 < s && s <= val+1) {
								overSec = true;
								rd = true;
							}
						}
						if(rd) clockComposite.redraw();
					}
					break;
				case SWT.MouseUp:
					setH = setM = setS = false;
					clockComposite.setCursor(event.display.getSystemCursor(SWT.CURSOR_ARROW));
					break;
				case SWT.MouseWheel:
					long time = getSelection().getTime();
					time += (event.count > 0) ? timeInc : -timeInc;
					setSelection(new Date(time), false, NOTIFY_REGULAR);
					break;
				case SWT.Paint:
					Rectangle r = clockComposite.getClientArea();
					Image timage = new Image(event.display, r.width, r.height);
					GC gc = new GC(timage);
					
					gc.setBackground(clockComposite.getBackground());
					gc.fillRectangle(r);
					
					gc.setAntialias(SWT.ON);
					gc.setTextAntialias(SWT.ON);

					// Paint Clock Face
					int dia = 2 * clockRadius;
					int x = clockCenter.x - clockRadius;
					int y = clockCenter.y - clockRadius;
					gc.setBackground(event.display.getSystemColor(SWT.COLOR_GRAY));
					gc.fillOval(x, y, dia, dia);
					gc.drawOval(x, y, dia, dia);
					gc.setBackground(event.display.getSystemColor(SWT.COLOR_WHITE));
					gc.fillOval(x+4, y+4, dia-8, dia-8);
					gc.drawOval(x+4, y+4, dia-8, dia-8);
					int inc = 36;
					for(int i = 0; i < inc; i++) {
						int x2 = x + clockRadius + (int)((clockRadius-10) * (Math.cos(2 * (double)i * Math.PI / inc)));
						int y2 = y + clockRadius + (int)((clockRadius-10) * (Math.sin(2 * (double)i * Math.PI / inc)));
						gc.drawLine(clockCenter.x, clockCenter.y, x2, y2);
					}
					gc.setBackground(event.display.getSystemColor(SWT.COLOR_WHITE));
					gc.fillOval(x+13, y+13, dia-26, dia-26);
					inc = 12;
					for(int i = 0; i < inc; i++) {
						int x2 = x + clockRadius + (int)((clockRadius-8) * (Math.cos(2 * (double)i * Math.PI / inc)));
						int y2 = y + clockRadius + (int)((clockRadius-8) * (Math.sin(2 * (double)i * Math.PI / inc)));
						gc.drawLine(clockCenter.x, clockCenter.y, x2, y2);
					}
					gc.fillOval(x+15, y+15, dia-30, dia-30);
					tmpcal.set(1, 1, 1, 0, 0, 0);
					SimpleDateFormat sdf = new SimpleDateFormat("h", locale);
					gc.setForeground(event.display.getSystemColor(SWT.COLOR_DARK_BLUE));
					gc.setAlpha(200);
					inc = 12;
					for(int i = 0; i < inc; i++) {
						int x2 = x + clockRadius + (int)((clockRadius-25) * 
								(Math.cos(2 * (double)i * Math.PI / inc - Math.PI/2)));
						int y2 = y + clockRadius + (int)((clockRadius-25) * 
								(Math.sin(2 * (double)i * Math.PI / inc - Math.PI/2)));
						String str = sdf.format(tmpcal.getTime());
						Point ss = gc.stringExtent(str);
						gc.drawString(str, x2-(ss.x/2), y2-(ss.y/2));
						tmpcal.add(Calendar.HOUR_OF_DAY, 1);
					}

					// paint "AM_PM"
					tmpcal.setTime(getSelection());
					sdf.applyPattern("a"); //$NON-NLS-1$
					String str = sdf.format(tmpcal.getTime());
					Point ss = gc.stringExtent(str);
					gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK));
					gc.setAlpha(255);
					gc.drawString(str, x+clockRadius-(ss.x/2), y+clockRadius+(clockRadius/3)-(ss.y/2));

					double i;
					int x2, y2;

					// paint Second Hand
					if(secHand) {
						if(overSec) {
							gc.setBackground(event.display.getSystemColor(SWT.COLOR_BLACK));
							gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK));
							gc.setLineWidth(4);
							gc.setAlpha(255);
						} else {
							gc.setForeground(event.display.getSystemColor(SWT.COLOR_DARK_GRAY));
							gc.setLineWidth(2);
							gc.setAlpha(175);
						}
						inc = 60;
						i = tmpcal.get(Calendar.SECOND);
						x2 = x + clockRadius + (int)((clockRadius-10) * 
								(Math.cos(2 * i * Math.PI / inc - Math.PI/2)));
						y2 = y + clockRadius + (int)((clockRadius-10) * 
								(Math.sin(2 * i * Math.PI / inc - Math.PI/2)));
						gc.drawLine(clockCenter.x, clockCenter.y, x2, y2);
						if(overSec) gc.fillOval(x2-2, y2-2, 4, 4);
					}
					
					// paint Minute Hand
					if(overMin) {
						gc.setBackground(event.display.getSystemColor(SWT.COLOR_BLACK));
						gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK));
						gc.setLineWidth(4);
						gc.setAlpha(255);
					} else {
						gc.setForeground(event.display.getSystemColor(SWT.COLOR_DARK_BLUE));
						gc.setLineWidth(2);
						gc.setAlpha(200);
					}
					inc = 60;
					i = tmpcal.get(Calendar.MINUTE);
					x2 = x + clockRadius + (int)((clockRadius-17) * 
							(Math.cos(2 * i * Math.PI / inc - Math.PI/2)));
					y2 = y + clockRadius + (int)((clockRadius-17) * 
							(Math.sin(2 * i * Math.PI / inc - Math.PI/2)));
					gc.drawLine(clockCenter.x, clockCenter.y, x2, y2);
					if(overMin) gc.fillOval(x2-2, y2-2, 4, 4);

					// paint Hour Hand
					if(overHour) {
						gc.setBackground(event.display.getSystemColor(SWT.COLOR_BLACK));
						gc.setForeground(event.display.getSystemColor(SWT.COLOR_BLACK));
						gc.setLineWidth(4);
					} else {
						gc.setForeground(event.display.getSystemColor(SWT.COLOR_DARK_BLUE));
						gc.setLineWidth(2);
					}
					gc.setAlpha(255);
					inc = 12;
					i = tmpcal.get(Calendar.HOUR);
					i += (double) tmpcal.get(Calendar.MINUTE) / 60;
					x2 = x + clockRadius + (int)((clockRadius-35) * 
							(Math.cos(2 * i * Math.PI / inc - Math.PI/2)));
					y2 = y + clockRadius + (int)((clockRadius-35) * 
							(Math.sin(2 * i * Math.PI / inc - Math.PI/2)));
					gc.drawLine(clockCenter.x, clockCenter.y, x2, y2);
					if(overHour) gc.fillOval(x2-2, y2-2, 4, 4);

					// Paint "Go-To-Current-Time" Button
					gc.setBackground(event.display.getSystemColor(SWT.COLOR_BLACK));
					gc.fillOval(clockCenter.x-5, clockCenter.y-5, 10, 10);

					gc.setBackground(event.display.getSystemColor(SWT.COLOR_GRAY));
					gc.fillOval(clockCenter.x-4, clockCenter.y-4, 8, 8);

					clockNow.setImage(timage);
					clockAmPm.setImage(timage, 0, clockAmPm.getLocation().y - clockCenter.y + clockAmPm.getSize().y/2);
					event.gc.drawImage(timage, 0, 0);
					gc.dispose();
					timage.dispose();
					break;
				default:
				}
			}
		};
		clockComposite.addListener(SWT.MouseDown, listener);
		clockComposite.addListener(SWT.MouseMove, listener);
		clockComposite.addListener(SWT.MouseUp, listener);
		clockComposite.addListener(SWT.MouseWheel, listener);
		clockComposite.addListener(SWT.Paint, listener);
	}

	/**
	 * notify listeners of a selection event.
	 * <dt><b>Valid Notifications:</b></dt>
	 * <dd>NOTIFY_NONE, NOTIFY_REGULAR, NOTIFY_DEFAULT</dd>
	 * If <code>notification</code> equals NOTIFY_NONE then this method simply returns.
	 * @param notification the type of notification
	 */
	private void fireSelectionChanged(int notification) {
		if(notification == NOTIFY_NONE) return;
		Event event = new Event();
		event.data = getSelection();
		notifyListeners((notification == NOTIFY_DEFAULT) ? SWT.DefaultSelection : SWT.Selection, event);
	}

	/**
	 * Returns an array of all locales for which there is a corresponding properties file (translation).
	 * <p>The properties file contains localized translations for the buttons.  If your locale is not
	 * returned and you would like to have it included, please submit a feature request and attach a
	 * completed translation (http://www.sourceforge.net/projects/calypsorcp).</p>
	 * <p>Time and Date displays will still work with Locales not returned in this array as they are
	 * handled by the Java Calendar classes.  Call <code>Calendar.getAvailableLocales()</code> for
	 * an array of these system supported Locales.</p>
	 * @return an array of fully supported Locale objects
	 * @see Calendar#getAvailableLocales()
	 */
	public Locale[] getAvailableLocales() {
		return new Locale[] {
			    Locale.ENGLISH,
			    new Locale("ro"),
			    Locale.US
				};
	}
	
	/**
	 * Get the number of milliseconds that are used to increment and
	 * decrement the time on the Clock (eg. using the mouse wheel).
	 * @return a long representing the increment in milliseconds
	 * @see #setClockIncrement(long)
	 */
	public long getClockIncrement() {
		return timeInc;
	}

	/**
	 * Get the visibilty setting of the Footer for the Calendar part of this CDatepicker.
	 * The Clock part has no equivalent so this function may return true even
	 * when the Footer is actually not showing due to the Calendar not showing.
	 * @return true if the Footer is set to show, false otherwise
	 * @see #isFooterVisible()
	 * @see #setFooterVisible(boolean)
	 */
	public boolean getFooterVisible() {
		return footerVisible;
	}
	
	/**
	 * Get the format style of this CDatepicker. If the format supports dates, the Calendar is shown;
	 * if time then the Clock is shown. If the style supports both, then both the Calendar and the Clock 
	 * are shown side-by-side.
	 * <dt><b>Valid Styles:</b></dt>
	 * <dd>DATE_SHORT, DATE_MEDIUM, DATE_LONG, DATE_CUSTOM, TIME_SHORT, TIME_MEDIUM, TIME_CUSTOM</dd>
	 * @return the display style of this CDatepicker
	 * @see #setFormat(int)
	 */
	public int getFormat() {
		return format;
	}

	/**
	 * Get the visibilty setting of the Grid for the Calendar part of this CDatepicker.
	 * The Clock part has no equivalent so this function may return true even
	 * when the Grid is actually not showing due to the Calendar not showing.
	 * @return true if the Grid is set to show, false otherwise
	 * @see #isGridVisible()
	 * @see #setGridVisible(boolean)
	 */
	public boolean getGridVisible() {
		return gridVisible;
	}

	/**
	 * The locale currently in use by this CDatepicker
	 * @return the locale
	 * @see #setLocale(Locale)
	 */
	public Locale getLocale() {
		return locale;
	}

	/**
	 * Get the default setting for the use ofthe Clock's Seconds Hand
	 * @return true if the Seconds Hand's default is on; false otherwise
	 */
	public boolean getSecondsHandDefault() {
		return secHandDefault;
	}
	
	/**
	 * The <code>java.util.Date</code> that is currently selected by this
	 * CDatepicker widget.<br>
	 * @return the date, guaranteed to never be <code>null</code>
	 * @see #setSelection(Date)
	 * @see #setSelection(Date, boolean)
	 */
	public Date getSelection() {
		Date date = (Date) dayButtons[selDayButton].getData(DATE);
		if(date == null) date = new Date();
		tmpcal.setTime(date);
		tmpcal.set(Calendar.HOUR_OF_DAY, hourOfDay);
		tmpcal.set(Calendar.MINUTE, minute);
		tmpcal.set(Calendar.SECOND, second);
		return tmpcal.getTime();
	}

	/**
	 * Get the snap intervals used when setting the minutes and seconds.
	 * @return an int[2] -> int[0] is the minutes snap, and int[1] is
	 * the seconds snap
	 * @see #setTimeSnap(int, int)
	 */
	public int[] getTimeSnap() {
		return timeSnap;
	}
	
	/**
	 * Focus Event handler
	 * @param type focus event type: SWT.FocusIn or SWT.FocusOut
	 */
	private void handleFocus(int type) {
		if (isDisposed ()) return;
		switch (type) {
		case SWT.FocusIn:
			if(editYear && 
					!yearEditControls.contains(getDisplay().getFocusControl())) {
				setEditYearMode(false, true);
			}
			if(hasFocus) return;
			hasFocus = true;
			Display display = getDisplay ();
			display.removeFilter (SWT.FocusIn, filter);
			display.addFilter (SWT.FocusIn, filter);
			Event e = new Event ();
			notifyListeners (SWT.FocusIn, e);
			break;
		case SWT.FocusOut:
			if(editYear && !yearEditControls.contains(getDisplay().getFocusControl())) {
				setEditYearMode(false, true);
			}
			if(!hasFocus) return;
			if(controls.contains(getDisplay().getFocusControl())) return;
			hasFocus = false;
			display = getDisplay();
			display.removeFilter (SWT.FocusIn, filter);
			e = new Event ();
			notifyListeners (SWT.FocusOut, e);
			break;
		}
	}
	
	/**
	 * Determines whether or not the footer actually is visible rather than just set to visible.
	 * This method may return false even though the footer has been set visible because the
	 * Calendar itself (upon which the footer is drawn) is not visible.
	 * @return true if the footer is visible, false otherwise
	 * @see #getFooterVisible()
	 * @see #setFooterVisible(boolean)
	 */
	public boolean isFooterVisible() {
		return footerVisible && (format & (ACW.DATE_SHORT | ACW.DATE_MEDIUM | ACW.DATE_LONG | ACW.DATE_CUSTOM)) != 0;
	}
	
	/**
	 * Determines whether or not the grid actually is visible rather than just set to visible.
	 * This method may return false even though the grid has been set visible because the
	 * Calendar itself (upon which the grid is drawn) is not visible.
	 * @return true if the grid is visible, false otherwise
	 * @see #getGridVisible()
	 * @see #setGridVisible(boolean)
	 */
	public boolean isGridVisible() {
		return gridVisible && (format & (ACW.DATE_SHORT | ACW.DATE_MEDIUM | ACW.DATE_LONG | ACW.DATE_CUSTOM)) != 0;
	}
	
	/**
	 * Removes the listener from the collection of listeners who will
	 * be notified when the receiver's selection changes.
	 * @param listener the listener which should no longer be notified
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
	 * </ul>
	 * @exception SWTException <ul>
	 *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
	 *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that 
	 *    created the receiver</li>
	 * </ul>
	 * @see SelectionListener
	 * @see #addSelectionListener
	 */
	public void removeSelectionListener(SelectionListener listener) {
		checkWidget ();
		if(listener != null) {
			removeListener(SWT.Selection, listener);
			removeListener(SWT.DefaultSelection, listener);
		}
	}

	/**
	 * Scroll the Calendar's visible days just as a user would with the keyboard.
	 * <dt><b>Valid Keys:</b></dt>
	 * <dd>SWT.ARROW_UP, SWT.ARROW_DOWN, SWT.END, SWT.HOME, SWT.PAGE_DOWN, SWT.PAGE_UP</dd>
	 * @param keycode a SWT keycode
	 * @see #traverseSelection(int)
	 */
	public void scrollCalendar(int keycode) {
		scrollCalendar(keycode, false);
	}
	
	/**
	 * perform the scroll by making a call to  {@link #adjustDays(int, int)} with the
	 * <code>field</code> set to Calendar.DATE and the <code>amount</code> 
	 * corresponding to the keycode.
	 */
	private void scrollCalendar(int keycode, boolean notify) {
		boolean update = false;
		switch (keycode) {
		case SWT.ARROW_DOWN:
			adjustDays(Calendar.DATE, 7);
			update = true;
			break;
		case SWT.ARROW_UP:
			adjustDays(Calendar.DATE, -7);
			update = true;
			break;
		case SWT.END:
			adjustDays(Calendar.DATE, 52*7);
			update = true;
			break;
		case SWT.HOME:
			adjustDays(Calendar.DATE, -52*7);
			update = true;
			break;
		case SWT.PAGE_DOWN:
			adjustDays(Calendar.DATE, 4*7);
			update = true;
			break;
		case SWT.PAGE_UP:
			adjustDays(Calendar.DATE, -4*7);
			update = true;
			break;
		}
		
		if(update) {
			updateHeader();
			updateDays();
		}
	}

	/**
	 * Sets the <code>cancelDate</code> variable to the currently selected date,
	 * as found with {@link CDatepicker#getSelection()}.
	 * The cancelDate variable is the Date to which this CDatepicker's selection will be 
	 * "reset" to upon a cancellation event, such as when the user selects of the 
	 * Clock's "Cancel" button.<br>
	 * Note that the "Cancel" button will not be visible unless <code>comboMode</code> is true.
	 * <p>ONLY INTENDED FOR USE BY CDATEPICKERCOMBO</p>
	 * @see CDatepickerCombo
	 * @see #setComboMode()
	 */
	void setCancelDate() {
		cancelDate = getSelection();
		clockCancel.setEnabled(true);
	}
	
	/**
	 * Set the milliseconds to be used when incrementing/decrementing the
	 * time on the Clock (eg. with the mouse wheel).  If the given value
	 * is less than or equal to zero then it will be reset to its default
	 * value of 30000 (5 minutes).
	 * <p>Note that if a snap is set that is more than twice the increment
	 * then incrementing will appear to not work (because the value will be 
	 * incremented and the snap will pull it right back).</p>
	 * @param millis the number of milliseconds to increment or decrement
	 * @see #getClockIncrement()
	 */
	public void setClockIncrement(long millis) {
		timeInc = (millis < 0) ? 30000 : millis;
	}

	/**
	 * Set the <code>comboMode</code> variable true, indicating that this CDatepicker is
	 * being used as the drop_down component in a CDatepickerCombo.
	 * <p>ONLY INTENDED FOR USE BY CDATEPICKERCOMBO</p>
	 * @see CDatepickerCombo
	 * @see #setCancelDate()
	 */
	void setComboMode() {
		comboMode = true;
		clockAccept.setToolTipText(Messages.getString("accept", locale));//$NON-NLS-1$
		clockCancel.setToolTipText(Messages.getString("cancel", locale));//$NON-NLS-1$
		clockComposite.layout(true, true);
		clockAccept.setVisible(true);
		clockCancel.setVisible(true);
	}

	/**
	 * Set the date for each dayButton by starting with the given <code>firstDate</code>
	 * and iterating over all the dayButtons, adding 1 day to the date with each iteration.<br>
	 * The date is stored in the dayButton with: setData(DATE, date).<br>
	 * If <code>alignMonth</code> is true, then the actual first date used will be modified
	 * to be the first date of the visible calendar which includes the given 
	 * <code>firstDate</code>
	 * @param firstDate the first date of the dayButtons
	 * @param alignMonth whether or not to align the month
	 */
	private void setDays(Date firstDate, boolean alignMonth) {
		tmpcal.setTime(firstDate);

		if(alignMonth) {
			tmpcal.set(Calendar.DATE, 1);
			int firstDay = tmpcal.get(Calendar.DAY_OF_WEEK) - tmpcal.getFirstDayOfWeek();
			if(firstDay < 0) {
				firstDay += 7;
			}
			tmpcal.add(Calendar.DATE, -firstDay);
		}
		
		for(int day = 0; day < dayButtons.length; day++) {
			dayButtons[day].setData(DATE, tmpcal.getTime());
			tmpcal.add(Calendar.DATE, 1);
		}
	}

	/**
	 * Set the EditYearMode on or off.  If setting it off, accept or cancel.
	 * @param edit enter edit mode if true, exit otherwise
	 * @param cancel if exiting edit mode, cancel the edit if true, accept otherwise; 
	 * has no affect if entering edit mode (<code>edit</code> is true)
	 */
	private void setEditYearMode(boolean edit, boolean cancel) {
		if(editYear == edit) return;
		if(edit) { // cancel has no meaning here
			yearText.setText(yearLabel.getText());

			Rectangle tBounds = yearLabel.getBounds();
			Point lSize = yearLabel.getSize();
			Point tSize = yearComposite.computeSize(-1, -1);
			tBounds.x = tBounds.x - ((tSize.x-lSize.x)/2);
			tBounds.y = tBounds.y - ((tSize.y-lSize.y)/2);
			tBounds.width = tSize.x;
			tBounds.height = tSize.y;
			
			yearComposite.setBounds(tBounds);

			yearLabel.setVisible(false);
			yearComposite.setVisible(true);
			yearText.selectAll();
			yearText.setFocus();

			tBounds.width = yearEditCancel.computeSize(-1, -1).x;
			tBounds.x -= tBounds.width;
			yearEditCancel.setBounds(tBounds);
			tBounds.width = yearEditAccept.computeSize(-1, -1).x;
			tBounds.x -= tBounds.width;
			yearEditAccept.setBounds(tBounds);
			monthLabel.setVisible(false);
			yearEditCancel.setVisible(true);
			yearEditAccept.setVisible(true);
		} else {
			if(cancel) {
				yearText.setText(yearLabel.getText());
			} else {
				tmpcal.setTime(getSelection());
				tmpcal.set(Calendar.YEAR, Integer.parseInt(yearText.getText()));
				setSelection(tmpcal.getTime(), true, NOTIFY_REGULAR);
			}
			monthLabel.setVisible(true);
			yearLabel.setVisible(true);
			yearComposite.setVisible(false);
			yearEditCancel.setVisible(false);
			yearEditAccept.setVisible(false);
			header.layout();
		}
		editYear = edit;
	}
	
	public boolean setFocus() {
		return dayButtons[selDayButton].setFocus();
	}

	/**
	 * Set whether or not the Footer is visible.<br>
	 * Note that the Footer only applies to the Calendar part of the
	 * <code>CDatepicker</code>; if this part is not visible, this method will
	 * appear to have no affect.
	 * @param visible whether or not the Footer should be visible
	 * @see #getFooterVisible()
	 * @see #isFooterVisible()
	 */
	public void setFooterVisible(boolean visible) {
		if(footerVisible != visible) {
			footerVisible = visible;
			((GridData) footerButton.getLayoutData()).exclude = !visible;
			updateFooter();
			layout(true, true);
		}
	}

	/**
	 * Set the format style of this CDatepicker to support dates and / or times.
	 * If the format supports dates, the Calendar will be shown; if time then the Clock will be
	 * shown. If the format supports both, then both the Calendar and the Clock will be shown
	 * side-by-side.  If the format supports time but does not support seconds (ie. SHORT)
	 * then the Clock's Seconds Hand will not be drawn or active regardless of its default.
	 * <p>If no valid format is given then the default of DATE_SHORT is set.</p>
	 * <dt><b>Valid Styles:</b></dt>
	 * <dd>DATE_SHORT, DATE_MEDIUM, DATE_LONG, DATE_CUSTOM, TIME_SHORT, TIME_MEDIUM, TIME_CUSTOM</dd>
	 * @param format the format to be set
	 * @see #getFormat()
	 * @see #setSecondsHandDefault(boolean)
	 * @see CDatepickerCombo#setFormat(int)
	 * @see CDatepickerCombo#setFormat(String)
	 */
	public void setFormat(int format) {
		int mask = (ACW.DATE_SHORT | ACW.DATE_MEDIUM | ACW.DATE_LONG | ACW.DATE_CUSTOM |
				ACW.TIME_SHORT | ACW.TIME_MEDIUM | ACW.TIME_CUSTOM);
		this.format = format & mask;
		boolean date = (this.format & (ACW.DATE_SHORT | ACW.DATE_MEDIUM | ACW.DATE_LONG | ACW.DATE_CUSTOM)) != 0;
		boolean time = (this.format & (ACW.TIME_SHORT | ACW.TIME_MEDIUM | ACW.TIME_CUSTOM)) != 0;
		if(date && time) {
			baseLayout.numColumns = 2;
			((GridData) calendarComposite.getLayoutData()).exclude = false;
			((GridData) clockComposite.getLayoutData()).exclude = false;
			calendarComposite.setVisible(true);
			clockComposite.setVisible(true);
		} else {
			baseLayout.numColumns = 1;
			if(time) {
				((GridData) calendarComposite.getLayoutData()).exclude = true;
				((GridData) clockComposite.getLayoutData()).exclude = false;
				calendarComposite.setVisible(false);
				clockComposite.setVisible(true);
			} else { // default style is date
				((GridData) calendarComposite.getLayoutData()).exclude = false;
				((GridData) clockComposite.getLayoutData()).exclude = true;
				calendarComposite.setVisible(true);
				clockComposite.setVisible(false);
			}
		}
		if(time && ((this.format & ACW.TIME_SHORT) != 0)) {
			secHand = false;
		} else {
			secHand = secHandDefault;
		}
		layout(true, true);
	}

	/**
	 * Set whether or not the Grid is visible.<br>
	 * Note that the Grid only applies to the Calendar part of the
	 * <code>CDatepicker</code>; if this part is not visible, this method will
	 * appear to have no affect.
	 * @param visible whether or not the Grid should be visible
	 */
	public void setGridVisible(boolean visible) {
		gridVisible = visible;
		body.redraw();
	}
	
	/**
	 * Sets the Locale to be used by this CDatepickerCombo and causes all affected 
	 * attributes to be updated<br>
	 * If the provided locale is the same as the current locale then this method simply
	 * returns.  If the provided Locale is null then this CDatepickerCombo will use
	 * the system's default locale.<br>
	 * @param locale the Locale, or null to use the system's default
	 * @see CDatepicker#setLocale(Locale)
	 */
	public void setLocale(Locale locale) {
		if(locale == null) locale = Locale.getDefault();
		if(locale != null) {
			this.locale = locale;
			updateLocale();
		}
	}

	/**
	 * Set whether or not the Clock's Seconds Hand will default to being used.
	 * <p>If the default is true and the format is set to either MEDIUM or CUSTOM
	 * then the Seconds Hand of the Clock will be visible and active.  If the
	 * default is false OR the format is set to SHORT (which does not have a
	 * seconds component) then the Seconds Hand of the Clock will NOT be visible
	 * nor will it be active.</p>
	 * @param on the default mode of the Clock's Seconds Hand; may be overridden by
	 * the setting of the format
	 * @see #setFormat(int)
	 */
	public void setSecondsHandDefault(boolean on) {
		secHand = secHandDefault = on;
		clockComposite.redraw();
	}
	
	/**
	 * Set the selection for this CDatepicker to that of the provided
	 * <code>Date</code> object and update all affected attributes.<br>
	 * Note that all Calendar fields will be set by this method, even if part of the
	 * CDatepicker is not visible - for instance, if the CDatepicker is of style DATE,
	 * and only the date part is visible, this method will still set the time fields.<br>
	 * The method is equivalent to calling <code>setSelection(Date, true)</code>.
	 * @param date the <code>Date</code> object to use for the new selection
	 * @see #getSelection()
	 * @see #setSelection(Date, boolean)
	 */
	public void setSelection(Date date) {
		setSelection(date, true);
	}
	
	/**
	 * Set the selection for this CDatepicker to that of the provided
	 * <code>Date</code> object and update all affected attributes.<br>
	 * Note that all Calendar fields will be set by this method, even if part of the
	 * CDatepicker is not visible - for instance, if the CDatepicker is of style DATE,
	 * and only the date part is visible, this method will still set the time fields.<br>
	 * If <code>alignMonth</code> is true then the visible calendar will be centered on the 
	 * month containing the given date, otherwise it will scroll by week so that the given
	 * date is visible.
	 * @param date the <code>Date</code> object to use for the new selection
	 * @param alignMonth whether or not the month should be aligned like a standard printed
	 * calendar would be.
	 * @see #getSelection()
	 * @see #setSelection(Date)
	 */
	public void setSelection(Date date, boolean alignMonth) {
		if(!getSelection().equals(date)) {
			tmpcal.setTime(date);
			setSelection(tmpcal.getTime(), alignMonth, NOTIFY_NONE);
		}
	}

	/**
	 * if the selection is different from the current, then updates the days,
	 * figures out which button corresponds to the given date, and calls
	 * {@link #setSelection(int, int)} to set it.
	 */
	private void setSelection(Date date, boolean alignMonth, int notification) {
		Calendar cal = Calendar.getInstance(locale);
		cal.setTime(snap(date));
		if(hourOfDay != cal.get(Calendar.HOUR_OF_DAY)) hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
		if(minute != cal.get(Calendar.MINUTE)) minute = cal.get(Calendar.MINUTE);
		if(second != cal.get(Calendar.SECOND)) second = cal.get(Calendar.SECOND);
		clockComposite.redraw();
		if(!getSelection().equals(cal.getTime())) {

			if(alignMonth || cal.before(getSelection()) || cal.after(getSelection())) {
				setDays(cal.getTime(), true);
			}
			
			for(int i = 0; i < dayButtons.length; i++) {
				tmpcal.setTime((Date) dayButtons[i].getData(DATE));
				if((cal.get(Calendar.DATE) == tmpcal.get(Calendar.DATE)) &&
						(cal.get(Calendar.MONTH) == tmpcal.get(Calendar.MONTH)) &&
						(cal.get(Calendar.YEAR) == tmpcal.get(Calendar.YEAR)) ) {
					setSelection(i, notification);
					break;
				}
			}
			updateHeader();
			updateDays();
		}
	}
	
	/**
	 * actually sets the given dayButton as selected and fires a selection event notification
	 */

	private void setSelection(int dayButton, int notification) {
		if(selDayButton != dayButton && dayButton >= 0 && dayButton < dayButtons.length) {
			int old = selDayButton;
			selDayButton = dayButton;
			dayButtons[old].setSelection(false);
			dayButtons[selDayButton].setSelection(true);
			dayButtons[selDayButton].setFocus();
			tmpcal.setTime(getSelection());
			int year = tmpcal.get(Calendar.YEAR);
			int month = tmpcal.get(Calendar.MONTH);
			tmpcal.setTime((Date) dayButtons[old].getData(DATE));
			if(((tmpcal.get(Calendar.YEAR)  != year) ||
				(tmpcal.get(Calendar.MONTH) != month)) ) {
				updateHeader();
				updateDays();
			}
		}
		fireSelectionChanged(notification);
	}

	/**
	 * Set the snap for the minutes and seconds.  If the value given for 
	 * either parameter is less than or equal to zero then its corresponding 
	 * snap will be set to its default of one (1).
	 * @param min the snap interval for the minutes
	 * @param sec the snap interval for the seconds
	 * @see #getTimeSnap()
	 */
	public void setTimeSnap(int min, int sec) {
		timeSnap[0] = (min < 0) ? 1 : min;
		timeSnap[1] = (sec < 0) ? 1 : sec;
	}

	/**
	 * perform the snap and return a new "snapped" Date object
	 */
	private Date snap(Date date) {
		tmpcal.setTime(date);

		int v = tmpcal.get(Calendar.MINUTE);
		int m = v % timeSnap[0];
		if(m != 0) {
			v += (m > timeSnap[0]/2) ? (timeSnap[0]-m) : -m;
			if(v > 59) v = 0;
			tmpcal.set(Calendar.MINUTE, v);
		}
		
		v = tmpcal.get(Calendar.SECOND);
		m = v % timeSnap[1];
		if(m != 0) {
			v += (m > timeSnap[1]/2) ? (timeSnap[1]-m) : -m;
			if(v > 59) v = 0;
			tmpcal.set(Calendar.SECOND, v);
		}

		return tmpcal.getTime();
	}
	
	/**
	 * Traverse the selection programmatically just as a user would with the keyboard.
	 * <dt><b>Valid Keys:</b></dt>
	 * <dd>SWT.ARROW_UP, SWT.ARROW_DOWN, SWT.ARROW_LEFT, SWT.ARROW_RIGHT</dd>
	 * @param keycode a SWT traversal keycode
	 * @see #scrollCalendar(int)
	 */
	public void traverseSelection(int keycode) {
		traverseSelection(keycode, NOTIFY_NONE);
	}
	
	/**
	 * perform the traversal by calling the appropriate {@link #setSelection(int, int)}
	 * or {@link #scrollCalendar(int)} method.
	 */
	private void traverseSelection(int keycode, int src) {
		switch (keycode) {
		case SWT.ARROW_UP:
			if(selDayButton > DAYS_IN_WEEK) {
				setSelection(selDayButton - DAYS_IN_WEEK, src);
			} else {
				scrollCalendar(SWT.ARROW_UP);
			}
			break;
		case SWT.ARROW_DOWN:
			if(selDayButton < DAYS_IN_WEEK * (NUM_ROWS-1)) {
				setSelection(selDayButton + DAYS_IN_WEEK, src);
			} else {
				scrollCalendar(SWT.ARROW_DOWN);
			}
			break;
		case SWT.ARROW_LEFT:
			if(selDayButton > 0) {
				setSelection(selDayButton - 1, src);
			} else {
				scrollCalendar(SWT.ARROW_UP);
				setSelection(selDayButton + (DAYS_IN_WEEK-1), src);
			}
			break;
		case SWT.ARROW_RIGHT:
			if(selDayButton < (DAYS_IN_WEEK * NUM_ROWS - 1)) {
				setSelection(selDayButton + 1, src);
			} else {
				scrollCalendar(SWT.ARROW_DOWN);
				setSelection(selDayButton - (DAYS_IN_WEEK-1), src);
			}
		}
	}

	/**
	 * set / update the text of the <code></code>displayLabels.  these are the Week 
	 * column headers above the days on the Calendar part of the <code>CDatepicker</code>.
	 */
	private void updateDayLabels() {
		tmpcal = Calendar.getInstance(locale);
		SimpleDateFormat sdf = new SimpleDateFormat("E", locale);
		tmpcal.set(Calendar.DAY_OF_WEEK, tmpcal.getFirstDayOfWeek());
		for(int x = 0; x < dayLabels.length; x++) {
			dayLabels[x].setText(sdf.format(tmpcal.getTime()).substring(0,1));
			tmpcal.add(Calendar.DAY_OF_WEEK, 1);
		}
	}

	/**
	 * set / update the text and font color of the <code>dayButton</code>s.
	 * currently done in an async runnable because it can be a costly operation.
	 * TODO: udpateDays() should to be redone.
	 */
	private void updateDays() {
		getDisplay().asyncExec(new Runnable() {
			public void run() {
				CButton dayButton;
				SimpleDateFormat sdf = new SimpleDateFormat("d", locale);
				Calendar today = Calendar.getInstance(locale);
				tmpcal.setTime(getSelection());
				int year = tmpcal.get(Calendar.YEAR);
				int month = tmpcal.get(Calendar.MONTH);
				for(int day = 0; day < dayButtons.length; day++) {
					if((dayButtons[day] != null) && !dayButtons[day].isDisposed()) {
						dayButton = dayButtons[day];
						
						// get date
						tmpcal.setTime((Date) dayButton.getData(DATE));
	
						// set font color
						if((tmpcal.get(Calendar.YEAR) == today.get(Calendar.YEAR)) && 
								(tmpcal.get(Calendar.DAY_OF_YEAR) == today.get(Calendar.DAY_OF_YEAR))) {
							dayButton.setForeground(getDisplay().getSystemColor(SWT.COLOR_RED));
						} else if(!((tmpcal.get(Calendar.YEAR) == year) &&
									(tmpcal.get(Calendar.MONTH) == month)) ) {
							dayButton.setForeground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
						} else {
							dayButton.setForeground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_FOREGROUND));
						}
						
						// set label
						dayButton.setText(sdf.format(tmpcal.getTime()));
					}
				}
			}
		});
	}
	
	/**
	 * set / update the text of the <code>footerButton</code>.
	 */
	private void updateFooter() {
		if(footerVisible) {
			Calendar cal = Calendar.getInstance(locale);
			Object[] margs = {
					cal.getTime(),
					Messages.getString("date_ordinal_" + cal.get(Calendar.DATE), locale)
					};
			MessageFormat formatter = new MessageFormat(Messages.getString("label_today", locale), locale);//$NON-NLS-1$
			footerButton.setText(formatter.format(margs));
			footerButton.getParent().layout();
		}
	}
	
	/**
	 * set / update the text of the header - <code>monthLabel</code>, <code>yearLabel</code>,
	 * and the <code>monthLabel</code> context menu.
	 */
	private void updateHeader() {
		SimpleDateFormat sdf = new SimpleDateFormat("MMMM", locale);//$NON-NLS-1$
		monthLabel.setText(sdf.format(getSelection()));
		for(int i = 0; i < 12; i++) {
			tmpcal.set(Calendar.MONTH, i);
			monthItems[i].setText(sdf.format(tmpcal.getTime()));
			monthItems[i].setData("Month", new Integer(tmpcal.get(Calendar.MONTH)));//$NON-NLS-1$
		}

		sdf.applyPattern("yyyy");//$NON-NLS-1$
		yearLabel.setText(sdf.format(getSelection()));
		
		header.layout();
	}

	/**
	 * set / update, or calls methods to set / update, all components affected by the <code>locale</code>
	 * @see updateHeader();
	 * @see updateDayLabels();
	 * @see updateDays();
	 * @see updateFooter();
	 */
	private void updateLocale() {
		tmpcal = Calendar.getInstance(locale);
		monthPrev.setToolTipText(Messages.getString("nav_prev_month", locale));//$NON-NLS-1$
		calendarNow.setToolTipText(Messages.getString("nav_current_day", locale));//$NON-NLS-1$
		monthNext.setToolTipText(Messages.getString("nav_next_month", locale));//$NON-NLS-1$
		yearEditAccept.setToolTipText(Messages.getString("accept", locale));//$NON-NLS-1$
		yearEditCancel.setToolTipText(Messages.getString("cancel", locale));//$NON-NLS-1$
		clockNow.setToolTipText(Messages.getString("nav_current_time", locale));//$NON-NLS-1$
		if(comboMode) {
			clockAccept.setToolTipText(Messages.getString("accept", locale));//$NON-NLS-1$
			clockAccept.setToolTipText(Messages.getString("cancel", locale));//$NON-NLS-1$
		}
		footerButton.setToolTipText(Messages.getString("nav_current_day", locale));//$NON-NLS-1$
		bodyItem.setText(Messages.getString("nav_current_day", locale));//$NON-NLS-1$

		updateHeader();
		updateDayLabels();
		setDays(getSelection(), true);
		updateDays();
		updateFooter();
	}
}